#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/select.h>
#include <sys/time.h>

#include <process.h>
#include <log.h>

struct ProcessData {
    pid_t               pid;

    pid_t               pid_bak;

    int                 infd;

    int                 outfd;

    int                 errfd;

    ProcessData() : pid(-1)
                  , pid_bak(-1)
                  , infd(-1)
                  , outfd(-1)
                  , errfd(-1)
    {
    }
};

Process::Process() : _sys(NULL), _user_data(NULL)
{
    ProcessData *pd = new ProcessData();

    if (pd == NULL) {
        log_print("Process::Process: alloc memory failed: %s\n",
                  strerror(errno));
        return;
    }

    _sys = (void *) pd;
}

Process::~Process()
{
    ProcessData *pd = (ProcessData *) _sys;

    if (pd) {
        clean_resources();
        delete pd;
    }
}

bool Process::run(std::string name,
                  Strings&    args,
                  int*        sin,
                  int*        sout,
                  int*        serr,
                  int         exit_code)
{
    _name = name;

    ProcessData *pd = (ProcessData *) _sys;

    if (pd == NULL) {
        pd = new ProcessData();
        if (pd == NULL)
            return false;
        _sys = (void *) pd;
    }

    clean_resources();

    // create the pipes
    int stdin_pipe[2]  = { -1, -1 };
    int stdout_pipe[2] = { -1, -1 };
    int stderr_pipe[2] = { -1, -1 };

    pid_t   pid;
    char  **argv;
    int     i;

    // argument checking
    if ((sin && *sin != PROC_BIND_PIPE && *sin != PROC_BIND_FD)
        || (sout && *sout != PROC_BIND_PIPE && *sout != PROC_BIND_FD)
        || (serr && *serr != PROC_BIND_PIPE && *serr != PROC_BIND_FD)) {
        log_print("Process::run(%s): invalid binding\n", _name.c_str());
        return false;
    }

    // create pipe early
    if (sin && *sin == PROC_BIND_PIPE) {
        if (pipe(stdin_pipe) < 0) {
            log_print("Process::run(%s): create pipe failed: %s\n",
                      _name.empty() ? "NULL" : _name.c_str(),
                      strerror(errno));
            return false;
        }
    }
    if (sout && *sout == PROC_BIND_PIPE) {
        if (pipe(stdout_pipe) < 0) {
            log_print("Process::run(%s): create pipe failed: %s\n",
                      _name.empty() ? "NULL" : _name.c_str(),
                      strerror(errno));
            goto pipe_in;
        }
    }
    if (serr && *serr == PROC_BIND_PIPE) {
        if (pipe(stderr_pipe) < 0) {
            log_print("Process::run(%s): create pipe failed: %s\n",
                      _name.empty() ? "NULL" : _name.c_str(),
                      strerror(errno));
            goto pipe_in_out;
        }
    }

    pid = fork();

    if (pid < 0) {
        log_print("Process::run(%s): fork failed: %s\n",
                  _name.empty() ? "NULL" : _name.c_str(),
                  strerror(errno));
        goto pipe_all;
    }

    // parent process
    if (pid > 0) {
        int in, out, err;

        in = out = err = -1;

        if (sin) {
            if (*sin == PROC_BIND_PIPE) {
                close(stdin_pipe[0]);
                in = stdin_pipe[1];
            }
            else
                in = dup(sin[1]);
        }

        if (sout) {
            if (*sout == PROC_BIND_PIPE) {
                close(stdout_pipe[1]);
                out = stdout_pipe[0];
            }
            else
                out = dup(sout[1]);
        }

        if (serr) {
            if (*serr == PROC_BIND_PIPE) {
                close(stderr_pipe[1]);
                err = stderr_pipe[0];
            }
            else
                err = dup(serr[1]);
        }

        pd->infd  = in;
        pd->outfd = out;
        pd->errfd = err;

        pd->pid = pid;
        pd->pid_bak = pid;

        if (pd->outfd >= 0)
            fcntl(pd->outfd, F_SETFL, O_NONBLOCK);
        if (pd->errfd >= 0)
            fcntl(pd->errfd, F_SETFL, O_NONBLOCK);

        _run_ticks = get_ticks();
    }
    else {
        // make the argument vector
        argv = (char **)
            malloc (sizeof (char *) * (args.size() + 2));
        for (i = 0; i < (int) args.size(); i++) {
            argv[i + 1] = strdup(args[i].c_str());
        }
        argv[i + 1] = NULL;
        argv[0] = strdup(name.c_str());

        // re-bind
        if (sin) {
            if (*sin == PROC_BIND_PIPE) {
                close(stdin_pipe[1]);
                dup2(stdin_pipe[0], 0);
            }
            else
                dup2(sin[1], 0);
        }

        if (sout) {
            if (*sout == PROC_BIND_PIPE) {
                close(stdout_pipe[0]);
                dup2(stdout_pipe[1], 1);
            }
            else
                dup2(sout[1], 1);
        }

        if (serr) {
            if (*serr == PROC_BIND_PIPE) {
                close(stderr_pipe[0]);
                dup2(stderr_pipe[1], 2);
            }
            else
                dup2(serr[1], 2);
        }

        if (strchr(name.c_str(), '/'))
            execv(name.c_str(), argv);
        else
            execvp(name.c_str(), argv);

        exit(exit_code);
    }

    return true;

pipe_all:
    close(stderr_pipe[0]);
    close(stderr_pipe[1]);

pipe_in_out:
    close(stdout_pipe[0]);
    close(stdout_pipe[1]);

pipe_in:
    close(stdin_pipe[0]);
    close(stdin_pipe[1]);

    return false;
}

bool Process::running()
{
    ProcessData *pd = (ProcessData *) _sys;

    return (pd && pd->pid >= 0);
}

int Process::write_stdin(const char *buf)
{
    ProcessData *pd = (ProcessData *) _sys;

    if (pd == NULL || pd->infd < 0)
        return 0;

    int len  = strlen(buf);
    int curr = 0;
    int fd   = pd->infd;

    while (curr < len) {
        int r = write(fd, buf + curr, len - curr);

        if (r > 0) {
            curr += r;
        }
        else if (r <= 0) {
            if (errno != EAGAIN && errno != EINTR) {
                log_print("Process::write_stdin(%s): write fd %d failed: %s\n",
                          _name.empty() ? "NULL" : _name.c_str(),
                          fd, strerror(errno));
                break;
            }
        }
    }

    return curr;
}

void Process::close_stdin()
{
    ProcessData *pd = (ProcessData *) _sys;

    if (pd && pd->infd >= 0) {
        close(pd->infd);
        pd->infd = -1;
    }
}

static int _read_fd(int fd, char *buf, int size)
{
    int curr;

    curr = 0;
    while (curr < size) {
        int r = read(fd, buf + curr, size - curr);

        if (r > 0)
            curr += r;
        else if (r == 0)
            break;
        else if (r < 0) {
            if (errno != EINTR) {
                break;
            }
        }
    }

    return curr;
}

int Process::read_stdout(char *buf, int size)
{
    ProcessData *pd = (ProcessData *) _sys;

    if (pd == NULL || pd->outfd < 0)
        return 0;

    return _read_fd(pd->outfd, buf, size);
}

int Process::read_stderr(char *buf, int size)
{
    ProcessData *pd = (ProcessData *) _sys;

    if (pd == NULL || pd->errfd < 0)
        return 0;

    return _read_fd(pd->errfd, buf, size);
}

int Process::wait_proc(int  flag,
                       int *exited,
                       int *exit_type,
                       int *exit_value)
{
    ProcessData *pd = (ProcessData *) _sys;

    if (pd == NULL) {
        *exited = 1;
        *exit_type = PROC_EXIT_NOT_START;
        return 0;
    }

    pid_t pid = pd->pid;

    if (pid < 0) {
        *exited = 1;
        *exit_type = PROC_EXIT_NOT_START;
        return 0;
    }

    // pid_t waitpid(pid_t pid, int *status, int options);
    int status = 0;
    pid_t r = waitpid(pid, &status,
                      ((flag == PROC_WAIT)
                       ? 0
                       : (WNOHANG | WUNTRACED)));

    if (r < 0) {
        log_print("Process::wait_proc(%s): wait proc %d failed, return %d: %s\n",
                  _name.empty() ? "NULL" : _name.c_str(),
                  pid, r, strerror(errno));
        pd->pid = -1;
        return -1;
    }
    else if (r != pid) {
        *exited = 0;
        return 0;
    }

    if (WIFEXITED(status)) {
        *exited = 1;
        *exit_type = PROC_EXIT_NORMAL;
        *exit_value = WEXITSTATUS(status);

        pd->pid = -1;
    }
    else if (WIFSIGNALED(status)) {
        *exited = 1;
        *exit_type = PROC_EXIT_SIGNAL;
        *exit_value = WTERMSIG(status);

        pd->pid = -1;
    }
    else if (WCOREDUMP(status)) {
        *exited = 1;
        *exit_type = PROC_EXIT_CORE_DUMP;

        pd->pid = -1;
    }
    else {
        log_print("Process::wait_proc(%s): got status %d\n",
                  _name.empty() ? "NULL" : _name.c_str(),
                  status);
    }

    return 0;
}

void Process::send_signal(int signo)
{
    ProcessData *pd = (ProcessData *) _sys;

    if (pd && pd->pid >= 0)
        kill(pd->pid, signo);
}

void Process::clean_resources()
{
    int exited;
    int exit_type;
    int exit_value;

    ProcessData *pd = (ProcessData *) _sys;

    if (pd == NULL)
        return;

    if (pd->pid >= 0) {
        if (wait_proc(PROC_NO_WAIT, &exited, &exit_type, &exit_value)) {
            if (!exited) {
                // send the kill signal to the child process
                kill(pd->pid, SIGKILL);

                wait_proc(PROC_WAIT, &exited, &exit_type, &exit_value);
            }

            pd->pid = -1;
        }
    }

    if (pd->infd >= 0) {
        close(pd->infd);
        pd->infd = -1;
    }

    if (pd->outfd >= 0) {
        close(pd->outfd);
        pd->outfd = -1;
    }

    if (pd->errfd >= 0) {
        close(pd->errfd);
        pd->errfd = -1;
    }
}
